package com.xiaozhu.wx;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xiaozhu.Utils.MessageUtils;
import com.xiaozhu.config.GetHttpSessionConfig;
import com.xiaozhu.pojo.ClientMessage;
import jakarta.servlet.http.HttpSession;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.http.WebSocket;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * ChatEndpoint
 *
 * WebSocket 聊天服务端
 *
 * 说明：
 * 1. @ServerEndpoint("/chat") 将普通类变成 WebSocket 服务端。
 *    前端可以通过 new WebSocket("ws://localhost:8080/chat") 连接。
 * 2. @Component创建改类的bean, 但是前提是使用ServerEndpointExporter，让Spring Boot扫描它加入到bean容器中
 *  可以在其他bean对象中通过调用 @Autowired 使用。
 * 3. 使用 @OnOpen、@OnMessage、@OnClose、@OnError 注解处理 WebSocket 生命周期事件。
 * /
 * 1️⃣ ChatEndpoint 的实例是由谁创建的？
 * WebSocket 的工作方式：每当一个客户端连接时，WebSocket 容器（如 Tomcat）会自动创建一个新的实例。
 * 也就是说你 不用自己 new MyChatEndpoint()，WebSocket 连接一建立，服务器就帮你创建一个实例。
 * 对应的 @OnOpen 方法会在实例创建后立即调用。
 * /
 * 实例是类的一个对象，它本身没有固定“名字”，你在 Map 里用 this 引用它存起来，或者用 Map 的 key（用户名）来管理。
 * 也就是说，类名只是告诉容器“创建哪种类型的对象”，而 this 是具体实例。
 * 类名像“模具”，实例像“从模具出来的零件”，每个零件都有自己的 session。
 * 你可以换类名，零件还是一样，只要 Map 泛型改成新的类名就行。
 */

//configurator = GetHttpSessionConfig.class
// → 告诉 WebSocket 容器 在创建这个 Endpoint 实例之前使用 GetHttpSessionConfig 类来定制或修改配置。

@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfig.class)
@Component
@Slf4j
public class MyChatEndpoint {

    /**
     * 在线用户集合，线程安全
     * key: 用户名
     * value: 对应 ChatEndpoint 实例
     * 每个用户连接 WebSocket 都会创建一个新的 ChatEndpoint 实例
     */
    private static Map<String, MyChatEndpoint> onlineUsers = new ConcurrentHashMap<>();

    /**
     * WebSocket Session 对象
     * 每个 ChatEndpoint 对应一个客户端会话，通过它可以发送消息给客户端
     */
    private Session session;

    /**
     * HttpSession 对象
     * 用于获取用户登录时存储的信息，比如用户名
     */
    private HttpSession httpSession;

    /**
     * 当客户端建立 WebSocket 连接时触发
     *
     * @param session WebSocket 会话对象
     * @param config  WebSocket 配置对象，用于获取握手时存储的属性
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        // 保存当前 WebSocket 会话
        this.session = session;

        // 从 EndpointConfig 中获取 HttpSession(EndpointConfig在握手时放入的HttpSession对象)
        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        if (httpSession == null) {
            log.error("HttpSession为空");
        }
        log.info("HttpSession: {}", httpSession);
        this.httpSession = httpSession;

        // 获取当前登录用户名
        String userName = (String) httpSession.getAttribute("user");
        log.info("当前上线用户: {}", userName);

        // 将用户和用户当前的ChatEndpoint对象加入在线集合
        // key → 用户
        //value → 当前用户的 ChatEndpoint 实例
        // 将当前用户加入到容器中
        onlineUsers.put(userName, this);

        // 构造系统消息（在线用户列表）
        String message = MessageUtils.getMessage(true, null, getNames());

        // 广播给所有在线用户
        broadcastAllUsers(message);
    }

    /**
     * 广播消息给所有在线用户
     *
     * @param message 消息内容
     */
    private void broadcastAllUsers(String message) {
        try {
            // 遍历在线用户集合
            for (String name : onlineUsers.keySet()) {
                MyChatEndpoint chatEndpoint = onlineUsers.get(name);
                // 同步发送消息
                chatEndpoint.session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            // 发送失败抛出运行时异常（也可以改为记录日志）
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取当前所有在线用户名集合
     *
     * @return Set<String> 在线用户名
     */
    private Set<String> getNames() {
        return onlineUsers.keySet();
    }

    /**
     * 当客户端发送消息时触发
     *
     * @param message 客户端发送的 JSON 消息字符串
     * @param session 当前 WebSocket 会话
     * 前端(用户)可以通过 new WebSocket("ws://localhost:8080/chat") 连接来发送信息给服务器
     * 服务器再将消息发送给指定的用户
     * 数据转换： json（用户发的数据）——转换成java对象(进行操作）——转成json对象发送给指定用户
     * 消息格式示例：
     * {"fromName":"Alice","toName":"Bob","message":"你好"}
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到消息: {}", message);
        try {
            // 使用 Jackson 将 JSON 转为 ClientMessage 对象
            ObjectMapper objectMapper = new ObjectMapper();
            ClientMessage clientMessage = objectMapper.readValue(message, ClientMessage.class);

            // 获取接收者用户名
            String toName = clientMessage.getToName();
            log.info("将数据发送给: {}", toName);

            // 获取消息内容
            String data = clientMessage.getMessage();
            log.info("发送的消息数据: {}", data);

            // 获取发送者用户名
            String fromName = clientMessage.getFromName();
            log.info("当前发送消息的用户: {}", fromName);

            // 封装消息为 JSON 字符串（可区分系统消息和普通消息）
            String result = MessageUtils.getMessage(false, fromName, data);

            // 将消息发送给指定用户
            onlineUsers.get(toName).session.getBasicRemote().sendText(result);

        } catch (Exception e) {
            // 捕获异常并抛出运行时异常（可改为日志记录或错误通知）
            throw new RuntimeException(e);
        }
    }

    /**
     * 当客户端关闭连接时触发
     *
     * @param session 当前 WebSocket 会话
     * @param closeReason 关闭原因
     */
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        // 1. 从HttpSession获取用户名
        String username = (String) httpSession.getAttribute("user");

        if (username != null) {
            // 2. 从在线用户集合移除
            onlineUsers.remove(username);

            /**
             * 每一个用户都有一个onlineUsers容器
             */

            // 3. 构造系统消息，标记为系统消息（system = true）
            // 前端解析时可以放到系统广播区域
            String systemMessage = MessageUtils.getMessage(true, null, getNames());

            // 4. 广播给所有在线用户
            broadcastAllUsers(systemMessage);
            
        }
    }

    /**
     * 当 WebSocket 出现异常时触发
     *
     * @param session 当前 WebSocket 会话
     * @param error 异常信息
     */
    @OnError
    public void onerror(Session session, Throwable error) {
        // TODO: 可记录日志或发送告警
        error.printStackTrace();
    }
}
  